查看原文
其他

【第1085期】React作者的构思和演绎

ramroll 前端早读课 2019-06-01

前言

周末了。今日早读文章由58同城前端架构师@ramroll翻译分享。

正文从这开始~

这是React作者在React设计之初,对整个框架的思考

我通过这篇文章试图阐述我对React模型的理解,阐述我们是如何用【演绎推导】来帮助我们得到最后的设计。

当然,这里有很多的前置条件是有争议的,而且这篇文章中的例子是有缺陷和漏洞。 但这是我们正式地去规范化它。如果你有更好的想法去形式化它,请随意向我们发PR。即便在没有太多库文件和细节的情况下,从简单到复杂的演绎应该是有意义的。

最终的React实现版本,需要大量务实的解决方案、增量迭代、算法优化、遗留代码、调试工具——这些如果有实用价值,都更新太快。所以react的实现过程是很难推导的。

所以我想给大家展示一个简单的构思模型让我可以立足其中。

Transformation(变换)

React的一个核心假设就是UI是数据到试图的映射。同样的输入总是可以得到同样的输出。

function NameBox(name) {
 return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };

抽象

你不可以在单个函数中使用复杂的UI。你需要先把界面抽象成为可以复用的部分,而且每个部分不泄露自己的实现细节(作为一个函数)。这样你就可以从一个函数调用另一个函数。

function FancyUserBox(user) {
 return {
   borderStyle: '1px solid blue',
   childContent: [
     'Name: ',
     NameBox(user.firstName + ' ' + user.lastName)
   ]
 };
}
{ firstName: 'Sebastian', lastName: 'Markbåge' } ->
{
 borderStyle: '1px solid blue',
 childContent: [
   'Name: ',
   { fontWeight: 'bold', labelContent: 'Sebastian Markbåge' }
 ]
};

上述代码FancyUserBox()和NameBox() 嵌套

Composition(组合)

为了实现真正可重用的特性,仅仅重用叶子节点,并在叶子节点上构造新的容器是不够的。 你需要在抽象的容器下面构造一个抽象容器的组合。 所谓的组合,就是将多个抽象合并成为一个新的抽象。

function FancyBox(children) {
 return {
   borderStyle: '1px solid blue',
   children: children
 };
}

function UserBox(user) {
 return FancyBox([
   'Name: ',
   NameBox(user.firstName + ' ' + user.lastName)
 ]);
}

其实这里说的组合就是多个组件(数组)可以作为一个整体渲染;这里说的抽象,其实就是组件。

State(状态)

UI不是简单地复制服务端的业务逻辑和状态,有很多状态是针对一个特殊的投射。比如在文本框中输入文字不会投射到其他tab或者设备上。 滚动位置不会在多个投射间复用。(小编:投射应该就是状态流过组件函数,最终被渲染的过程。输入框中被输入的文字和滚动位置这些状态太特殊了,是针对一种特殊的投射,没有办法从服务端复用,所以才需要状态)。

我们倾向于选择的数据模型是不可变的(immutable),我们将函数串联起来,并且认为更新状态的函数在最顶端。

function FancyNameBox(user, likes, onClick) {
 return FancyBox([
   'Name: ', NameBox(user.firstName + ' ' + user.lastName),
   'Likes: ', LikeBox(likes),
   LikeButton(onClick)
 ]);
}

// Implementation Details

var likes = 0;
function addOneMoreLike() {
 likes++;
 rerender();
}

// Init

FancyNameBox(
 { firstName: 'Sebastian', lastName: 'Markbåge' },
 likes,
 addOneMoreLike
);

这个例子在更新状态的时候有副作用。我真实的构思是在更新过程中函数返回下一个状态。这里是用最简单的方式给大家阐述原理,以后我们会更新这个例子。

Memoization(记忆)

调用一个纯函数一遍又一遍调用它其实非常浪费性能。我们可以把这个计算过程的输入和结果缓存起来。这样就不用重复计算。

function memoize(fn) {
 var cachedArg;
 var cachedResult;
 return function(arg) {
   if (cachedArg === arg) {
     return cachedResult;
   }
   cachedArg = arg;
   cachedResult = fn(arg);
   return cachedResult;
 };
}

var MemoizedNameBox = memoize(NameBox);

function NameAndAgeBox(user, currentTime) {
 return FancyBox([
   'Name: ',
   MemoizedNameBox(user.firstName + ' ' + user.lastName),
   'Age in milliseconds: ',
   currentTime - user.dateOfBirth
 ]);
}

Lists

很多UI都是列表——列表中的每一项有不同的数据,这构成了一种天然的结构。

为了管理每一个元素的状态,我们可以创造一个Map把每个元素的状态存起来。

function UserList(users, likesPerUser, updateUserLikes) {
 return users.map(user => FancyNameBox(
   user,
   likesPerUser.get(user.id),
   () => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
 ));
}

var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
 likesPerUser.set(id, likeCount);
 rerender();
}

UserList(data.users, likesPerUser, updateUserLikes);

我们将多个不同的参数传给了FancyNameBox,这个功能和记忆模块冲突了,因为我们每次只能记住一个值.接下来我们会讨论这点 .

Continuations(连续性)

不幸的是,UI中有太多的列表的列表,最后需要写很多的重复代码(boilerplate code)。

我们可以通过延迟执行函数,把一部分重复代码移出关键业务。例如:我们可以使用柯里化方法(bind in JavaScript)。这样当我们把状态从外部传入核心函数时不会遇到重复代码。

这种方法虽然没有消除重复代码,但至少将重复代码移出了核心业务逻辑。

function FancyUserList(users) {
 return FancyBox(
   UserList.bind(null, users)
 );
}

const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
const resolvedBox = {
 ...box,
 children: resolvedChildren
};

State Map(状态映射)

我们很早就知道,如果我们看到重复的模式,我们可以使用组合的方法避免重复实现这个模式。 我们可以把提取和传递状态的逻辑,移动到一个底层的函数中去。

function FancyBoxWithState(
 children,
 stateMap,
 updateState
) {
 return FancyBox(
   children.map(child => child.continuation(
     stateMap.get(child.key),
     updateState
   ))
 );
}

function UserList(users) {
 return users.map(user => {
   continuation: FancyNameBox.bind(null, user),
   key: user.id
 });
}

function FancyUserList(users) {
 return FancyBoxWithState.bind(null,
   UserList(users)
 );
}

const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);

Memoization Map

当我们试图记住列表中的多个项时,记忆变得更加困难。你需要找到一些复杂的缓存算法去平衡内存使用和频率。

庆幸的是,UI在同一位置是相对稳定的。树的相同位置总是得到相同的结果。这让树成为记忆的有效策略。

这样我们可以用同样的技术去缓存组合函数。

function memoize(fn) {
 return function(arg, memoizationCache) {
   if (memoizationCache.arg === arg) {
     return memoizationCache.result;
   }
   const result = fn(arg);
   memoizationCache.arg = arg;
   memoizationCache.result = result;
   return result;
 };
}

function FancyBoxWithState(
 children,
 stateMap,
 updateState,
 memoizationCache
) {
 return FancyBox(
   children.map(child => child.continuation(
     stateMap.get(child.key),
     updateState,
     memoizationCache.get(child.key)
   ))
 );
}

const MemoizedFancyNameBox = memoize(FancyNameBox);

Algebraic Effects

React看上去像个PITA饼,一层又一层的,一个很小的值也需要这样传递。 这样我们在不同层之间需要一个短路——"context"

有时候数据依赖并没有很好地遵循抽象树,举个例子,在布局算法中你需要事先知道子元素的的大小。

接下来的这个例子有点超出我们目前讨论的范围。我用Algebraic Effects(类似副作用的一个词汇),ECMAScript提出的来解释。如果你对函数式编程非常熟悉,他们在避免monads实践过程中炸了(they're avoiding the intermediate ceremony imposed by monads)。

function ThemeBorderColorRequest() { }

function FancyBox(children) {
 const color = raise new ThemeBorderColorRequest();
 return {
   borderWidth: '1px',
   borderColor: color,
   children: children
 };
}

function BlueTheme(children) {
 return try {
   children();
 } catch effect ThemeBorderColorRequest -> [, continuation] {
   continuation('blue');
 }
}

function App(data) {
 return BlueTheme(
   FancyUserList.bind(null, data.users)
 );
}

点评:

  • 大巧不工——被作者这样一说,复杂的东西其实也很简单。

  • 千里之行始于足下,学好简单的知识点,可以搞大事情

关于本文

译者:@ramroll

译文:https://zhuanlan.zhihu.com/p/30277192

原文:https://github.com/reactjs/react-basic/blob/master/README.md


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存